package deanandjames.ui;

import deanandjames.AsteroidsGame;
import deanandjames.configuration.Configuration;
import deanandjames.domain.AsteroidField;
import deanandjames.domain.SpaceShip;
import deanandjames.statistics.Score;
import deanandjames.statistics.Statistics;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

@SuppressWarnings("serial")
public class FullScreenGamePanel extends JPanel implements Runnable {
	private static final int FONT_SIZE = 24;
	private static final Color INFO_MESSAGE_COLOUR = Color.blue;
	private static int MAX_FRAME_SKIPS = 5;
	private static final int NO_DELAYS_PER_YIELD = 16;
	private static final int NUMBER_OF_ASTEROIDS = 10;

	// no. of frames that can be skipped in any one animation loop i.e the games state is updated but not rendered number of FPS values stored to get
	// an average
	private Thread animationThread;
	private volatile boolean animationThreadIsRunning = false;
	private final AsteroidField asteroidField;

	private final Background background;
	private Image doubleBufferingImage = null;
	private boolean finishedOff = false;
	private final Font font;
	private final FontMetrics fontMetrics;
	private volatile boolean gameOver = false;
	private final boolean isOverQuitButton = false;
	private volatile boolean isPaused = false;
	private Graphics2D offScreenRenderer;
	private final int panelHeight;

	private final int panelWidth;
	private final Rectangle pauseArea;
	private final long periodBetweenDrawingInNanos;
	private final Rectangle quitArea;

	private final Score score;
	private final SpaceShip spaceShip;

	private final Statistics statistics;

	public FullScreenGamePanel(AsteroidsGame wormChase, long period) {
		Configuration.configure(false);

		statistics = new Statistics();
		periodBetweenDrawingInNanos = period;
		final Toolkit toolKit = Toolkit.getDefaultToolkit();
		final Dimension screenDimensions = toolKit.getScreenSize();
		panelWidth = screenDimensions.width;
		panelHeight = screenDimensions.height;
		background = new Background(panelWidth, panelHeight, 20);
		setBackground(background.getColour());
		setPreferredSize(screenDimensions);
		setFocusable(true);
		// the JPanel now has focus, so receives key events
		requestFocus();
		readyForTermination();
		// create game components
		asteroidField = new AsteroidField(panelWidth, panelHeight, NUMBER_OF_ASTEROIDS);
		spaceShip = new SpaceShip(panelWidth, panelHeight);
		score = new Score();
		font = new Font("SansSerif", Font.BOLD, FONT_SIZE);
		fontMetrics = getFontMetrics(font);
		// specify screen areas for the buttons
		pauseArea = new Rectangle(panelWidth - 100, panelHeight - 45, 70, 15);
		quitArea = new Rectangle(panelWidth - 100, panelHeight - 20, 70, 15);
	}

	@Override
	public void addNotify() {
		// wait for the JPanel to be added to the JFrame before starting creates
		// the peer
		super.addNotify();
		if (animationThread == null || !animationThreadIsRunning) {
			// start the thread
			animationThread = new Thread(this);
			animationThread.start();
		}
	}

	private void drawButtons(Graphics g) {
		g.setColor(Color.white);
		g.drawOval(pauseArea.x, pauseArea.y, pauseArea.width, pauseArea.height);
		if (isPaused) {
			g.drawString("Paused", pauseArea.x, pauseArea.y + 10);
		} else {
			g.drawString("Pause", pauseArea.x + 5, pauseArea.y + 10);
		}
		if (isOverQuitButton) {
			g.setColor(Color.green);
		}
		g.drawOval(quitArea.x, quitArea.y, quitArea.width, quitArea.height);
		g.drawString("Quit", quitArea.x + 15, quitArea.y + 10);
	}

	private void finishOff() {
		// Tasks to do before terminating. Called at end of run() and via the shutdown hook in readyForTermination(). The call at the end of run() is
		// not really necessary, but included for safety. The flag stops the code being called twice.
		if (!finishedOff) {
			finishedOff = true;
			printStats();
			System.exit(0);
		}
	}

	private void gameOverMessage(Graphics g) {
		final String msg = "Game Over. Your Score: " + score;
		final int x = (panelWidth - fontMetrics.stringWidth(msg)) / 2;
		final int y = (panelHeight - fontMetrics.getHeight()) / 2;
		g.setColor(Color.red);
		g.setFont(font);
		g.drawString(msg, x, y);
	}

	private void gameRender() {
		if (doubleBufferingImage == null) {
			doubleBufferingImage = createImage(panelWidth, panelHeight);
			if (doubleBufferingImage == null) {
				System.out.println("dbImage is null");
				return;
			} else {
				offScreenRenderer = (Graphics2D) doubleBufferingImage.getGraphics();
			}
		}
		background.draw(offScreenRenderer);
		offScreenRenderer.setColor(INFO_MESSAGE_COLOUR);
		offScreenRenderer.setFont(font);
		renderStatistics();
		drawButtons(offScreenRenderer);
		if (gameOver) {
			gameOverMessage(offScreenRenderer);
		} else {
			asteroidField.draw(offScreenRenderer);
			spaceShip.draw(offScreenRenderer);
		}
		score.draw(offScreenRenderer, (int) spaceShip.getPosition().x, (int) (spaceShip.getPosition().y + 50));
	}

	private void gameUpdate() {
		if (!isPaused && !gameOver) {
			if (asteroidField.size() == 0) {
				gameOver = true;
			} else {
				asteroidField.removeAsteroidsThatCollideWithOtherAsteroids();

				if (spaceShip.getLaser() != null && !spaceShip.getLaser().isDisabled()) {
					if (asteroidField.removeAsteroidsThatCollide(spaceShip.getLaser())) {
						spaceShip.laserHit();
						score.increment();
					}
				}
				spaceShip.animate();
				asteroidField.animate();
				if (spaceShip.getLaser() != null) {
					spaceShip.getLaser().animate();
				}
				gameOver = asteroidField.doesAnythingCollide(spaceShip);
			}
		}
	}

	private boolean isQuitKeyPressed(KeyEvent e, final int keyCode) {
		return keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_Q || keyCode == KeyEvent.VK_END || keyCode == KeyEvent.VK_C && e.isControlDown();
	}

	private void paintScreen() {
		// use active rendering to put the buffered image on-screen
		try {
			Graphics2D graphics = (Graphics2D) getGraphics();
			if (graphics != null && doubleBufferingImage != null) {
				graphics.drawImage(doubleBufferingImage, 0, 0, null);
			}
			Toolkit.getDefaultToolkit().sync();
			graphics.dispose();

		} catch (final Exception e) {
			System.out.println("Graphics error: " + e);
		}
	}

	private void printStats() {
		System.out.println(statistics.getStats());
	}

	private void readyForTermination() {
		addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				final int keyCode = e.getKeyCode();
				if (isQuitKeyPressed(e, keyCode)) {
					animationThreadIsRunning = false;
				} else {
					Configuration configuration = Configuration.getInstance();
					switch (keyCode) {
						case KeyEvent.VK_DOWN:
							spaceShip.decellerate();
							break;
						case KeyEvent.VK_UP:
							spaceShip.accellerate();
							break;
						case KeyEvent.VK_LEFT:
							spaceShip.turnLeft();
							break;
						case KeyEvent.VK_RIGHT:
							spaceShip.turnRight();
							break;
						case KeyEvent.VK_SPACE:
							spaceShip.fireLaser();
							break;
						case KeyEvent.VK_D:
							configuration.setIsDebugEnabled(!configuration.isDebugEnabled());
							break;
					}
				}
			}
		});

		// for shutdown tasks a shutdown may not only come from the program
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				animationThreadIsRunning = false;
				finishOff();
			}
		});
	}

	private void renderStatistics() {
		offScreenRenderer.drawString(statistics.getStatsLine(), 20, 25);
		offScreenRenderer.drawString(statistics.getTimeSpentLine(), 10, panelHeight - 15);
	}

	public void run() {
		long overSleepTime = 0L;
		int noDelays = 0;
		long excess = 0L;
		statistics.setGameStartTime(System.nanoTime());
		long beforeTime = statistics.getGameStartTime();
		animationThreadIsRunning = true;
		while (animationThreadIsRunning) {
			gameUpdate();
			// render the game to a buffer
			gameRender();
			// draw the buffer on-screen
			paintScreen();
			long afterTime = System.nanoTime();
			long timeDiff = afterTime - beforeTime;
			long sleepTime = periodBetweenDrawingInNanos - timeDiff - overSleepTime;
			if (sleepTime > 0) {
				// some time left in this cycle
				try {
					Thread.sleep(sleepTime / 1000000L); // nano -> ms
				} catch (final InterruptedException ex) {
				}
				overSleepTime = System.nanoTime() - afterTime - sleepTime;
			} else {
				// sleepTime <= 0; the frame took longer than the periodBetweenDrawingInNanos store
				// excess time value
				excess -= sleepTime;
				overSleepTime = 0L;
				if (++noDelays >= NO_DELAYS_PER_YIELD) {
					// give another thread a chance to run
					Thread.yield();
					noDelays = 0;
				}
			}
			beforeTime = System.nanoTime();
			// If frame animation is taking too long, update the game state
			// without rendering it, to get the updates/sec nearer to the
			// required FPS.
			int skips = 0;
			while (excess > periodBetweenDrawingInNanos && skips < MAX_FRAME_SKIPS) {
				excess -= periodBetweenDrawingInNanos;
				// update state but don't render
				gameUpdate();
				skips++;
			}
			statistics.addSkippedFrames(skips);
			// excess = excess % periodBetweenDrawingInNanos;
			statistics.storeStats(periodBetweenDrawingInNanos);
		}
		finishOff();
	}
}